package com.immediatus.graphics.font;

import android.graphics.*;
import android.graphics.Paint.FontMetrics;
import android.graphics.Paint.Style;
import android.opengl.GLUtils;
import android.util.FloatMath;
import android.util.SparseArray;
import com.immediatus.graphics.contracts.ITexture;
import com.immediatus.util.Size;

import javax.microedition.khronos.opengles.GL10;
import java.util.ArrayList;


public class Font{
    protected static final float LETTER_LEFT_OFFSET = 0;
    protected static final int LETTER_EXTRA_WIDTH = 10;
    protected static final int PADDING = 1;

    private final Paint _paint;
    private final FontMetrics _fontMetrics;
    private final Canvas _canvas = new Canvas();
    private final ITexture _texture;
    private final float _textureWidth;
    private final float _textureHeight;
    private final SparseArray<Character> _managedCharacterToLetterMap = new SparseArray<Character>();
    private final ArrayList<Character> _lettersPendingToBeDrawnToTexture = new ArrayList<Character>();
    private final Paint _backgroundPaint;
    private final int _lineHeight;
    private final int _lineGap;
    private final Size _createLetterTemporarySize = new Size();
    private final Rect _getLetterBitmapTemporaryRect = new Rect();
    private final Rect _getStringWidthTemporaryRect = new Rect();
    private final Rect _getLetterBoundsTemporaryRect = new Rect();
    private final float[] _temporaryTextWidthFetchers = new float[1];

    private int _currentTextureX = 0;
    private int _currentTextureY = 0;

    public Font(final ITexture tex_, final Typeface typeface_, final float size_, final boolean antiAlias_, final int color_){
        this._texture = tex_;
        this._textureWidth = tex_.getWidth();
        this._textureHeight = tex_.getHeight();

        this._paint = new Paint();
        this._paint.setTypeface(typeface_);
        this._paint.setColor(color_);
        this._paint.setTextSize(size_);
        this._paint.setAntiAlias(antiAlias_);

        this._backgroundPaint = new Paint();
        this._backgroundPaint.setColor(Color.TRANSPARENT);
        this._backgroundPaint.setStyle(Style.FILL);

        this._fontMetrics = this._paint.getFontMetrics();
        this._lineHeight = (int) FloatMath.ceil(Math.abs(this._fontMetrics.ascent) + Math.abs(this._fontMetrics.descent)) + (PADDING * 2);
        this._lineGap = (int) (FloatMath.ceil(this._fontMetrics.leading));
    }

    public int getLineGap(){
        return this._lineGap;
    }

    public int getLineHeight(){
        return this._lineHeight;
    }

    public ITexture getTexture(){
        return this._texture;
    }

    public synchronized void reload(){
        final ArrayList<Character> lettersPendingToBeDrawnToTexture = this._lettersPendingToBeDrawnToTexture;
        final SparseArray<Character> managedCharacterToLetterMap = this._managedCharacterToLetterMap;

        for (int i = managedCharacterToLetterMap.size() - 1; i >= 0; i--){
            lettersPendingToBeDrawnToTexture.add(managedCharacterToLetterMap.valueAt(i));
        }
    }

    private int getLetterAdvance(final char ch_){
        this._paint.getTextWidths(String.valueOf(ch_), this._temporaryTextWidthFetchers);
        return (int) (FloatMath.ceil(this._temporaryTextWidthFetchers[0]));
    }

    private Bitmap getLetterBitmap(final char ch_){
        final Rect getLetterBitmapTemporaryRect = this._getLetterBitmapTemporaryRect;
        final String characterAsString = String.valueOf(ch_);
        this._paint.getTextBounds(characterAsString, 0, 1, getLetterBitmapTemporaryRect);

        getLetterBitmapTemporaryRect.right += PADDING * 2;

        final int lineHeight = this.getLineHeight();
        final Bitmap bitmap = Bitmap.createBitmap(getLetterBitmapTemporaryRect.width() == 0 ? 1 + (2 * PADDING) :
                getLetterBitmapTemporaryRect.width() + LETTER_EXTRA_WIDTH, lineHeight, Bitmap.Config.ARGB_8888);
        this._canvas.setBitmap(bitmap);
        this._canvas.drawRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), this._backgroundPaint);
        this.drawCharacterString(characterAsString);
        return bitmap;
    }

    protected void drawCharacterString(final String str_){
        this._canvas.drawText(str_, LETTER_LEFT_OFFSET + PADDING, -this._fontMetrics.ascent + PADDING, this._paint);
    }

    public int getStringWidth(final String txt_){
        this._paint.getTextBounds(txt_, 0, txt_.length(), this._getStringWidthTemporaryRect);
        return this._getStringWidthTemporaryRect.width();
    }

    private void getLetterBounds(final char ch_, final Size size_){
        this._paint.getTextBounds(String.valueOf(ch_), 0, 1, this._getLetterBoundsTemporaryRect);
        size_.set(
                this._getLetterBoundsTemporaryRect.width() + LETTER_EXTRA_WIDTH + (2 * PADDING), this.getLineHeight());
    }

    public void prepareLetters(final char... chs_){
        for (final char character : chs_){
            this.getLetter(character);
        }
    }

    public synchronized Character getLetter(final char ch_){
        final SparseArray<Character> managedCharacterToLetterMap = this._managedCharacterToLetterMap;
        Character ch = managedCharacterToLetterMap.get(ch_);
        if (ch == null){
            ch = this.createLetter(ch_);

            this._lettersPendingToBeDrawnToTexture.add(ch);
            managedCharacterToLetterMap.put(ch_, ch);
        }
        return ch;
    }

    public synchronized void update(final GL10 gl_){
        final ArrayList<Character> lettersPendingToBeDrawnToTexture = this._lettersPendingToBeDrawnToTexture;
        if (lettersPendingToBeDrawnToTexture.size() > 0){
            this._texture.bind(gl_);

            final float textureWidth = this._textureWidth;
            final float textureHeight = this._textureHeight;

            for (int i = lettersPendingToBeDrawnToTexture.size() - 1; i >= 0; i--){
                final Character ch = lettersPendingToBeDrawnToTexture.get(i);
                final Bitmap bitmap = this.getLetterBitmap(ch.getValue());

                GLUtils.texSubImage2D(GL10.GL_TEXTURE_2D, 0, (int) (ch.getTextureX() * textureWidth), (int) (
                        ch.getTextureY() * textureHeight), bitmap);

                bitmap.recycle();
            }
            lettersPendingToBeDrawnToTexture.clear();
            System.gc();
        }
    }

    private Character createLetter(final char ch_){
        final float textureWidth = this._textureWidth;
        final float textureHeight = this._textureHeight;

        final Size createLetterTemporarySize = this._createLetterTemporarySize;
        this.getLetterBounds(ch_, createLetterTemporarySize);

        final float letterWidth = createLetterTemporarySize.getWidth();
        final float letterHeight = createLetterTemporarySize.getHeight();

        if (this._currentTextureX + letterWidth >= textureWidth){
            this._currentTextureX = 0;
            this._currentTextureY += this.getLineGap() + this.getLineHeight();
        }

        final float letterTextureX = this._currentTextureX / textureWidth;
        final float letterTextureY = this._currentTextureY / textureHeight;
        final float letterTextureWidth = letterWidth / textureWidth;
        final float letterTextureHeight = letterHeight / textureHeight;

        final Character ch = new Character(ch_, this.getLetterAdvance(ch_), (int) letterWidth, (int) letterHeight, letterTextureX, letterTextureY, letterTextureWidth, letterTextureHeight);
        this._currentTextureX += letterWidth;

        return ch;
    }

    protected FontMetrics getFontMetrics(){
        return _fontMetrics;
    }

    protected Canvas getCanvas(){
        return _canvas;
    }
}